package com.foreverwin.mes.meapi.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.plugins.Page;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.foreverwin.mes.core.base.FrontPage;
import com.foreverwin.mes.core.exception.BusinessException;
import com.foreverwin.mes.core.utils.DateUtil;
import com.foreverwin.mes.core.utils.StringUtils;
import com.foreverwin.mes.meapi.dto.GenerateNextNumberRequest;
import com.foreverwin.mes.meapi.dto.GenerateNextNumberResponse;
import com.foreverwin.mes.meapi.mapper.NextNumberMapper;
import com.foreverwin.mes.meapi.model.NextNumber;
import com.foreverwin.mes.meapi.numbering.NextNumberBOHandle;
import com.foreverwin.mes.meapi.service.NextNumberService;
import com.foreverwin.mes.meapi.utils.NextNumberUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * 编号生成服务实现类
 * </p>
 *
 * @author syngna
 * @since 2019-01-10
 */
@Service
@Transactional
public class NextNumberServiceImpl extends ServiceImpl<NextNumberMapper, NextNumber> implements NextNumberService {

    private Logger logger = LoggerFactory.getLogger(getClass());
    private int maxIDLength = 2000;

    @Autowired
    private NextNumberMapper nextNumberMapper;

    @Override
    public Page<NextNumber> selectPage(FrontPage<NextNumber> frontPage) {
        EntityWrapper<NextNumber> entityWrapper = new EntityWrapper<NextNumber>();
        return super.selectPage(frontPage.getPagePlus(), entityWrapper);
    }

    @Override
    public List<NextNumber> selectList() {
        EntityWrapper<NextNumber> entityWrapper = new EntityWrapper<NextNumber>();
        return super.selectList(entityWrapper);
    }

    @Override
    public NextNumber selectByIdForUpdate(String handle) {
        return nextNumberMapper.selectByIdForUpdate(handle);
    }

    /**
     * 生成下一编号
     *
     * @param request
     * @return
     * @throws BusinessException
     */
    @Override
    public GenerateNextNumberResponse generateNextValues(GenerateNextNumberRequest request) throws BusinessException {
        GenerateNextNumberResponse res = formatNextValues(request);
        NextNumberBOHandle nnHandle = getEffective(request);

        Boolean warn = res.getGenereateWarning();
        if (warn != null && warn == true) {
            sendNextNumberWarning(request.getSite());
        }

        NextNumber nextNumber = new NextNumber();
        nextNumber.setHandle(nnHandle.getValue());
        nextNumber.setCurrentSequence(res.getCurrentSequence());
        nextNumber.setModifiedDateTime(new Date());
        int rowsChanged = nextNumberMapper.updateById(nextNumber);
        if (rowsChanged <= 0) {
            throw BusinessException.build("failed to update next number current sequence by id '" + nnHandle.getValue() + "'");
        }
        logger.debug(new StringBuilder().append("Updated Next Number in new txn (committed): ").append(nnHandle).toString());

        return res;
    }

    /**
     * 生成并格式化生成的编号
     *
     * @param request
     * @return
     * @throws BusinessException
     */
    public GenerateNextNumberResponse formatNextValues(GenerateNextNumberRequest request) throws BusinessException {
        GenerateNextNumberResponse res = new GenerateNextNumberResponse();
        Map<String, Object> customData = request.getCustomData();

        Map<String, Object> inData = (Map<String, Object>) JSON.toJSON(request);

        if (customData != null) {
            inData.remove("CUSTOM_DATA");
            customData.putAll(inData);
            inData = customData;
        }

        BigDecimal zero = BigDecimal.ZERO;

        int nValues = request.getNumberOfValues();
        BigDecimal nValuesBD = BigDecimal.valueOf(nValues);

        NextNumberBOHandle nnHandle = getEffective(request);
        //String nextNumberType = request.getNextNumberType();
        //String inn = getIncidentNumberTypeValue();
        logger.debug(new StringBuilder().append("nn:").append(nnHandle).toString());
        if (nnHandle == null) {
            throw BusinessException.build("Failed to get nextNumber def");
        }

        //boolean resetFlag = resetCurrentValue(nnHandle, inData.getString("INCREMENT_SEQUENCE", "TRUE"));
        String resetStr = (String) inData.get("INCREMENT_SEQUENCE");
        if (StringUtils.isBlank(resetStr)) {
            resetStr = "TRUE";
        }
        boolean resetFlag = resetCurrentValue(nnHandle, resetStr);
        if ((resetFlag) && (!"FALSE".equals(resetStr))) {
            resetFlag = false;
        }

        NextNumber nextNumberForUpdate = nextNumberMapper.selectByIdForUpdate(nnHandle.getValue());

        if (nextNumberForUpdate == null) {
            throw BusinessException.build("failed to get next number by handle '" + nnHandle.getValue() + "'");
        }
        logger.debug(new StringBuilder().append("nnData: ").append(JSON.toJSONString(nextNumberForUpdate, SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat)).toString());

        lockIdenticalNextNumberRecords(nnHandle, nextNumberForUpdate);

        String prefix = nextNumberForUpdate.getPrefix();
        String suffix = nextNumberForUpdate.getSuffix();
        String seqLength = nextNumberForUpdate.getSequenceLength();
        String seqBase = nextNumberForUpdate.getSequenceBase();
        if (StringUtils.isBlank(seqBase)) {
            seqBase = "10";
        }
        int seqLengthVal = 0;
        if (seqLength != null) {
            seqLengthVal = Integer.parseInt(seqLength);
        }
        String formatString = "";
        if (prefix != null) {
            formatString = prefix;
        }
        formatString = new StringBuilder().append(formatString).append("%CURRENT_SEQUENCE%").toString();
        if (suffix != null) {
            formatString = new StringBuilder().append(formatString).append(suffix).toString();
        }

        Date dateTime = (Date) inData.get("DATE_TIME");
        if ((dateTime == null)) {
            dateTime = new Date();
            inData.put("DATE_TIME", dateTime);
        }

        BigDecimal currentSeq = request.getSequence();
        if (currentSeq == null) {
            currentSeq = new BigDecimal(nextNumberForUpdate.getCurrentSequence());
        }

        String maxSeqStr = nextNumberForUpdate.getMaxSequence();
        String minSeqStr = nextNumberForUpdate.getMinSequence();
        if (StringUtils.isBlank(minSeqStr)) {
            minSeqStr = "1";
        }
        BigDecimal maxSeq;
        BigDecimal minSeq = new BigDecimal(minSeqStr);
        if (minSeq == null) {
            minSeq = BigDecimal.ONE;
        }

        if (StringUtils.isBlank(maxSeqStr)) {
            if (seqLengthVal > 0) {
                maxSeq = new BigDecimal(Math.pow(Integer.parseInt(seqBase), seqLengthVal) - 1.0D);
            } else {
                maxSeq = new BigDecimal(9223372036854775807L);
            }
        } else {
            maxSeq = new BigDecimal(maxSeqStr);
        }
        if (maxSeq.compareTo(BigDecimal.ZERO) == 0) {
            maxSeq = new BigDecimal(9223372036854775807L);
        }

        String incrementStr = nextNumberForUpdate.getIncr();
        ;
        if (StringUtils.isBlank(incrementStr)) {
            incrementStr = "1";
        }
        BigDecimal increment = new BigDecimal(incrementStr);

        String thresholdStr = nextNumberForUpdate.getWarningThreshold();
        if (StringUtils.isBlank(thresholdStr)) {
            thresholdStr = "0";
        }
        BigDecimal threshold = new BigDecimal(thresholdStr);

        BigDecimal currentSeqDB = new BigDecimal(nextNumberForUpdate.getCurrentSequence());
        BigDecimal thresholdSeq = maxSeq.subtract(threshold.multiply(increment));
        if ((currentSeqDB.compareTo(thresholdSeq) <= 0) && (currentSeq.add(nValuesBD.multiply(increment)).compareTo(thresholdSeq) > 0) && (threshold.intValue() != 0)) {
            res.setGenereateWarning(true);
        }

        if ((maxSeq != null) && (maxSeq.compareTo(zero) != 0)) {
            BigDecimal greatestGenNum = currentSeq.add(increment.multiply(nValuesBD.subtract(BigDecimal.ONE)));
            if (maxSeq.compareTo(greatestGenNum) < 0) {
                //if (inData.get("OBJECT_NAME") != null) throw new BasicBOBeanException(15073, inData);
                //throw new BasicBOBeanException(15020, inData);
                throw BusinessException.build("current sequence " + currentSeq + " max than max sequence " + maxSeqStr);
            }
        }

        Date dt = new Date();

        Calendar nonIsoCalendar = getNonISOCalendar(dt);
        Calendar isoCalendar = getISOCalendar(dt);

        int nonIsoWeekOfYear = nonIsoCalendar.get(3);
        inData.put("WEEKOFYEAR", Integer.toString(nonIsoWeekOfYear));

        int isoWeekOfYear = isoCalendar.get(3);
        inData.put("ISO_WEEKOFYEAR", Integer.toString(isoWeekOfYear));

        String nonIsoYear = Integer.toString(nonIsoCalendar.get(1));
        inData.put("ONEDIGITYEAR", nonIsoYear.substring(nonIsoYear.length() - 1));

        int isoYearInt = NextNumberUtil.getIsoYear(isoCalendar);
        String isoYear = Integer.toString(isoYearInt);
        inData.put("ISO_ONEDIGITYEAR", isoYear.substring(isoYear.length() - 1));

        inData.put("ISO_DATE_TIME", inData.get("DATE_TIME"));

        String activityName = nextNumberForUpdate.getActivity();

        int base = Integer.parseInt(nextNumberForUpdate.getSequenceBase());
        for (int i = 0; i < nValues; i++) {
            String seqS = NextNumberUtil.toStringWithBase(currentSeq.intValue(), base);

            if (seqLengthVal > 0)
                seqS = StringUtils.leftPad(seqS, "0", seqLengthVal);
            inData.put("CURRENT_SEQUENCE", seqS);

            //TODO 作业扩展
            if (activityName != null) {

            }
            /*if (activityName != null) {
                ActivityBOHandle handle = new ActivityBOHandle(activityName);
                ActivityBOInterface activity = (ActivityBOInterface)ServiceLocator.getService("ActivityBO");
                inData = activity.execute(handle, inData, activityName);
                if (inData == null) {
                    Data data = new Data("ACTIVITY_NAME", activityName);
                    abort(new BasicBOBeanException(19154, data));
                }
            }*/

            Object o = inData.get("CURRENT_SEQUENCE");
            if (!(o instanceof String)) {
                inData.put("CURRENT_SEQUENCE", seqS);
            }

            String id;
            try {
                logger.debug(new StringBuilder().append("inData: ").append(inData).toString());
                logger.debug((new StringBuilder().append("formatString: ").append(formatString).toString()));
                id = NextNumberUtil.replaceParameters(inData, formatString);
                logger.debug(new StringBuilder().append("id: ").append(id).toString());
                id = id.replaceAll("%", "");
                logger.debug(new StringBuilder().append("id2: ").append(id).toString());
            } catch (Exception e) {
                throw BusinessException.build("Format '" + formatString + "' error ", e);
            }
            res.getIds().add(id);
            /*if ((nextNumberType != null) && (inn != null) && (nextNumberType.trim().toUpperCase(Locale.ENGLISH).equals(inn.trim().toUpperCase(Locale.ENGLISH)))) {
                res.getIds().add(id);
            }
            else {
                res.getIds().add(id.toUpperCase(Locale.ENGLISH));
            }*/

            int max = getMaxIDLength();
            id = id.trim();
            if (id.length() > max) {
                throw BusinessException.build("Next number '" + id + "' length large than max length '" + max + "'");
            }

            if (!resetFlag) {
                currentSeq = currentSeq.add(increment);
            }
        }
        res.setNextNumberHandle(nnHandle.getValue());
        res.setCurrentSequence(currentSeq.toString());
        return res;
    }

    /**
     * 根据key获取handle值
     *
     * @param request site, nextNumberType
     * @return
     * @throws BusinessException
     */
    public NextNumberBOHandle getEffective(GenerateNextNumberRequest request) throws BusinessException {
        if (StringUtils.isBlank(request.getNextNumberType())) {
            throw BusinessException.build("field nextNumberType required");
        }
        return new NextNumberBOHandle(request.getSite(), request.getNextNumberType());
    }

    /**
     * 重置当前序号
     *
     * @param handle
     * @param incrementSequence
     * @return
     * @throws BusinessException
     */
    protected boolean resetCurrentValue(NextNumberBOHandle handle, String incrementSequence) throws BusinessException {
        NextNumber nextNumber = nextNumberMapper.selectByIdForUpdate(handle.getValue());

        /*if (isInContainer()) {
            stmt = Utils.selectForUpdate(stmt);
        }*/
        //Data nnData = executeQuery(stmt);

        if (nextNumber == null) {
            return Boolean.FALSE.booleanValue();
        }
        logger.debug(new StringBuilder().append("nnData: ").append(JSON.toJSONString(nextNumber, SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat)).toString());

        Date currentDT = new Date();

        String changeStampStr = nextNumber.getChangeStamp();
        Date lastUpdateDT;

        if (!StringUtils.isBlank(changeStampStr)) {
            lastUpdateDT = new Date(Long.parseLong(changeStampStr));
        } else {
            lastUpdateDT = nextNumber.getModifiedDateTime();
        }

        String reset = nextNumber.getReset();
        logger.debug(new StringBuilder().append("reset: ").append(reset).toString());
        logger.debug(new StringBuilder().append("lastUpdate: ").append(lastUpdateDT).toString());
        if (reset == null)
            reset = "N";
        if (shouldResetOccur(reset, lastUpdateDT, currentDT)) {
            if ("FALSE".equals(incrementSequence)) {
                return true;
            }

            String minSeq = nextNumber.getMinSequence();
            if (StringUtils.isBlank(minSeq)) {
                minSeq = "1";
            }

            NextNumber updatedNexNumber = new NextNumber();
            Date dateNow = new Date();
            updatedNexNumber.setHandle(handle.getValue());
            updatedNexNumber.setCurrentSequence(minSeq);
            updatedNexNumber.setModifiedDateTime(dateNow);
            updatedNexNumber.setChangeStamp(dateNow.getTime() + "");
            int rowsChanged = nextNumberMapper.updateById(updatedNexNumber);

            logger.debug(new StringBuilder().append("rowsChanged: ").append(rowsChanged).toString());
            if (rowsChanged <= 0) {
                throw BusinessException.build("failed to reset next number by id '" + "'" + handle.getValue());
            } else
                return Boolean.TRUE.booleanValue();
        }
        return Boolean.FALSE.booleanValue();
    }

    /**
     * 判断是否需要重置序号
     * N: 从不
     * A: 总是
     * D: 每天
     * W: 每周
     * M: 每月
     * Y: 每年
     * E: 每周 - 星期日
     * F: 每周 - 星期一
     * G: 每周 - 星期二
     * H: 每周 - 星期三
     * I: 每周 - 星期四
     * J; 每周 - 星期五
     * K: 每周 - 星期六
     *
     * @param reset
     * @param lastUpdateDT
     * @param currentDT
     * @return
     */
    public boolean shouldResetOccur(String reset, Date lastUpdateDT, Date currentDT) {
        if (reset.equals("A"))
            return true;
        if ((reset.equals("D")) && !DateUtil.sameDate(lastUpdateDT, currentDT))
            return true;
        if ((reset.equals("W")) && !DateUtil.sameWeek(lastUpdateDT, currentDT))
            return true;
        if ((reset.equals("M")) && !DateUtil.sameMonth(lastUpdateDT, currentDT))
            return true;
        if ((reset.equals("Y")) && (!DateUtil.sameYear(lastUpdateDT, currentDT))) {
            return true;
        }

        if ((reset.equals("E")) && (dayResetNeeded(lastUpdateDT, currentDT, "E"))) {
            return true;
        }
        if ((reset.equals("F")) && (dayResetNeeded(lastUpdateDT, currentDT, "F"))) {
            return true;
        }
        if ((reset.equals("G")) && (dayResetNeeded(lastUpdateDT, currentDT, "G"))) {
            return true;
        }
        if ((reset.equals("H")) && (dayResetNeeded(lastUpdateDT, currentDT, "H"))) {
            return true;
        }
        if ((reset.equals("I")) && (dayResetNeeded(lastUpdateDT, currentDT, "I"))) {
            return true;
        }
        if ((reset.equals("J")) && (dayResetNeeded(lastUpdateDT, currentDT, "J"))) {
            return true;
        }
        if ((reset.equals("K")) && (dayResetNeeded(lastUpdateDT, currentDT, "K"))) {
            return true;
        }

        return false;
    }

    /**
     * 是否需要按固定周天重置
     *
     * @param lastUpdateDT
     * @param currentDT
     * @param resetDay
     * @return
     */
    public boolean dayResetNeeded(Date lastUpdateDT, Date currentDT, String resetDay) {
        Calendar currentDateTime = Calendar.getInstance();
        currentDateTime.setTime(currentDT);
        currentDateTime.set(currentDateTime.get(Calendar.YEAR), currentDateTime.get(Calendar.MONTH), currentDateTime.get(Calendar.DATE), 23, 59, 59);

        Calendar lastUPdateDateTime = Calendar.getInstance();
        lastUPdateDateTime.setTime(lastUpdateDT);

        long diffDays = (lastUPdateDateTime.getTimeInMillis() - currentDateTime.getTimeInMillis()) / (1000 * 3600 * 24);
        int daysBetween = (int) diffDays;

        String lastUpdateDate = new StringBuilder().append(lastUPdateDateTime.get(Calendar.YEAR)).append(lastUPdateDateTime.get(Calendar.MONTH)).append(lastUPdateDateTime.get(Calendar.DATE)).toString();
        String currentDate = new StringBuilder().append(currentDateTime.get(Calendar.YEAR)).append(currentDateTime.get(Calendar.MONTH)).append(currentDateTime.get(Calendar.DATE)).toString();
        if (lastUpdateDate.equals(currentDate)) {
            return false;
        }

        if (daysBetween < -6) {
            return true;
        }

        Calendar nextDay = Calendar.getInstance();
        nextDay.setTime(lastUpdateDT);

        int dayDiffCount = Math.abs(daysBetween);
        int resetDow = getDayNumberFromResetDay(resetDay);
        for (int i = 0; i < dayDiffCount; i++) {
            nextDay.add(Calendar.DATE, 1);
            int dayNumber = nextDay.get(Calendar.DAY_OF_WEEK);
            if (dayNumber == resetDow) {
                return true;
            }
        }

        return false;
    }

    public int getDayNumberFromResetDay(String reset) {
        if (reset.equals("E")) {
            return 0;
        }
        if (reset.equals("F")) {
            return 1;
        }
        if (reset.equals("G")) {
            return 2;
        }
        if (reset.equals("H")) {
            return 3;
        }
        if (reset.equals("I")) {
            return 4;
        }
        if (reset.equals("J")) {
            return 5;
        }
        if (reset.equals("K")) {
            return 6;
        }
        return -1;
    }

    private List<String> lockIdenticalNextNumberRecords(NextNumberBOHandle nnHandle, NextNumber nextNumber) {
        if (nextNumber.getPrefix() == null) {
            nextNumber.setPrefix("");
        }
        if (nextNumber.getSuffix() == null) {
            nextNumber.setSuffix("");
        }
        List<String> list = nextNumberMapper.selectIdentical(nnHandle.getValue(), nextNumber.getPrefix(), nextNumber.getSuffix(), false);
        if (list != null && !list.isEmpty()) {
            nextNumberMapper.selectIdentical(nnHandle.getValue(), nextNumber.getPrefix(), nextNumber.getSuffix(), true);
        }
        return list;
    }

    private Calendar getNonISOCalendar(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.setLenient(true);
        return calendar;
    }

    private Calendar getISOCalendar(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.setLenient(true);
        calendar.setFirstDayOfWeek(2);
        calendar.setMinimalDaysInFirstWeek(4);
        return calendar;
    }

    private Map<String, Object> convertDataToCurrentBase(Map<String, Object> data) {
        int base = ((BigDecimal) data.get("SEQUENCE_BASE")).intValue();

        BigDecimal temp = (BigDecimal) data.get("INCR");
        if (temp != null) {
            long i = temp.longValue();
            data.put("INCR", NextNumberUtil.toStringWithBase(i, base));
        }

        temp = (BigDecimal) data.get("MIN_SEQUENCE");
        if (temp != null) {
            long i = temp.longValue();
            data.put("MIN_SEQUENCE", NextNumberUtil.toStringWithBase(i, base));
        }

        temp = (BigDecimal) data.get("MAX_SEQUENCE");
        if (temp != null) {
            long i = temp.longValue();
            data.put("MAX_SEQUENCE", NextNumberUtil.toStringWithBase(i, base));
        }

        temp = (BigDecimal) data.get("WARNING_THRESHOLD");
        if (temp != null) {
            long i = temp.longValue();
            data.put("WARNING_THRESHOLD", NextNumberUtil.toStringWithBase(i, base));
        }

        long i = ((BigDecimal) data.get("CURRENT_SEQUENCE")).longValue();
        data.put("CURRENT_SEQUENCE", NextNumberUtil.toStringWithBase(i, base));

        return data;
    }

    private Map<String, Object> convertDataToDecimalBase(Map<String, Object> data) {
        BigDecimal bd;
        if (data.get("INCR") != null) {
            bd = new BigDecimal((String) data.get("INCR"));
            data.put("INCR", new StringBuilder().append(bd.longValue()).toString());
        }
        if (data.get("MIN_SEQUENCE") != null) {
            bd = new BigDecimal((String) data.get("MIN_SEQUENCE"));
            data.put("MIN_SEQUENCE", new StringBuilder().append(bd.longValue()).toString());
        }
        if (data.get("MAX_SEQUENCE") != null) {
            bd = new BigDecimal((String) data.get("MAX_SEQUENCE"));
            data.put("MAX_SEQUENCE", new StringBuilder().append(bd.longValue()).toString());
        }
        if (data.get("WARNING_THRESHOLD") != null) {
            bd = new BigDecimal((String) data.get("WARNING_THRESHOLD"));
            data.put("WARNING_THRESHOLD", new StringBuilder().append(bd.longValue()).toString());
        }
        if (data.get("CURRENT_SEQUENCE") != null) {
            bd = new BigDecimal((String) data.get("CURRENT_SEQUENCE"));
            data.put("CURRENT_SEQUENCE", new StringBuilder().append(bd.longValue()).toString());
        }

        return data;
    }

    protected int getMaxIDLength() {
        if (this.maxIDLength > 0) {
            return this.maxIDLength;
        }
        //TODO read config file
        return this.maxIDLength;
    }

    private void sendNextNumberWarning(String site) {
        //TODO send alarm
    }
}